#include "functionParserWrapper.hpp"
#include "exceptions.hpp"
#include "formatHelpers.hpp"

#include <iostream>
#include <algorithm>

namespace hms
{

/**
 * @brief Returns the number of specified whitespace characters
 * at the beginning of a string.
 * 
 * @param str 
 * @param whitespace 
 * @return std::size_t 
 */
std::size_t nLeftWhitespace(
	const std::string& str,
	const std::string& whitespace = " \t\n"
){
	return str.find_first_not_of(whitespace);
}

/**
 * @brief Removes whitespace at the beginning and end of a string.
 * Whitespace can be specified
 * and defaults to the space, tab and newline characters.
 * 
 * @param str is the string to be trimmed.
 * @param whitespace 
 */
void trim(
	std::string& str,
	const std::string& whitespace = " \t\n"
){
    auto strBegin = str.find_first_not_of(whitespace);
	/* if pure whitespace, erase whole string */
    if (strBegin == std::string::npos){
        str = {};
		return;
	}

	str.erase( str.begin(), str.begin()+strBegin );

    auto strEnd = str.find_last_not_of(whitespace);
	str.erase( str.begin() + strEnd + 1, str.end() );
}

/**
 * @brief Convert tab character in string to a specified number of spaces
 * 
 * @param str is the string to be converted
 * @param nSpaces is the number of spaces to replace each tab character
 */
void tabsToSpaces( std::string& str, std::size_t nSpaces = 4 ){
	std::size_t pos;
	std::string replacement (nSpaces, ' ');
	while ( (pos = str.find('\t')) != std::string::npos ){
		str.replace(pos, 1, replacement);
	}
}

/**
 * @brief Goes through a string line by line and removes at most the number of
 * whitespace characters specified by baseIndent from the beginning of the line.
 * 
 * @param str 
 * @param baseIndent 
 */
void trimIndent( std::string& str, std::size_t baseIndent ){
	std::string out;
	out.reserve( str.size() );
	std::istringstream iss {str};
	std::size_t lineIndent;
	for ( std::string line; std::getline(iss, line); ){
		lineIndent = nLeftWhitespace(line);
		if (lineIndent != std::string::npos){
			line.erase(
				line.begin(),
				line.begin()+std::min(baseIndent, lineIndent)
			);
		}
		out += line + '\n';
	}
	/* remove last newline character */
	out.pop_back();
	out.shrink_to_fit();
	str = std::move(out);
}

/**
 * @brief Returns the number of lines in a string,
 * by counting the line breaks and adding 1.
 * The last character is ignored, so if it is a line break,
 * it does not count as another line (as it is empty).
 * 
 * @param str 
 * @return std::string::difference_type 
 */
long nLines( const std::string& str ){
	if ( str.empty() )
		return 0;
	return std::count(str.begin(), str.end() - 1, '\n') + 1;
}

/**
 * @brief Inserts the specified amount of indentation at the beginning of
 * every line of the string. Optionally skips first line.
 * 
 * @param str 
 * @param indentSize 
 * @param indentChar 
 */
void addIndent(
	std::string& str,
	std::size_t indentSize,
	char indentChar = ' ',
	bool skipFirstLine = false
){
	std::string out;
	out.reserve( str.size() + nLines(str) * indentSize );
	std::string indent ( indentSize, indentChar );

	std::istringstream iss {str};
	for ( std::string line; std::getline(iss, line); ){
		if (skipFirstLine){
			skipFirstLine = false;
			out += line + '\n';
			continue;
		}
		out += indent + line + '\n';
	}
	/* remove last newline character */
	out.pop_back();
	out.shrink_to_fit();
	str = std::move(out);
}

/**
 * @brief Composes an error message for when a FunctionParser parsing error
 * occurred.
 * The error message points to the location of the error, even when the
 * function string consists of multiple lines.
 * 
 * @param functionStr 
 * @param errPos 
 * @param errorMsg 
 * @param baseIndent 
 */
std::string composeFParserError(
	std::string& functionStr,
	int errPos,
	std::string&& errorMsg,
	std::size_t baseIndent
){
	static constexpr std::string_view
		msg1 {"Function string"},
		msg2 {"Error at pos. "};
	static constexpr std::size_t
		npos { std::string::npos },
		diff { msg1.size() - msg2.size() };

	/* convert error position to string to get number of digits,
	 * so that we can then align everything */
	std::string
		errPosStr { std::to_string(errPos) },
		pad (std::size_t{errPosStr.size() - diff}, ' ');

	/* get the line with the error */
	long lineNo { nLines( functionStr.substr(0, errPos) ) };

	/* Now we change the indentation to fit our alignment */
	trimIndent(functionStr, baseIndent);
	/* the + 2 is from ": " in the error message. */
	std::size_t indentSize { msg1.size() + pad.size() + 2 };
	addIndent(functionStr, indentSize, ' ', true);

	/* by changing the indentation, the error position has changed,
	 * so we recalculate it here */
	long newErrPos { errPos - std::max(0l, (lineNo - 1)) *
		( static_cast<long>(baseIndent) - static_cast<long>(indentSize) )
	};

	/* get line breaks before and after error position,
	 * so that we can split the message there and actually point to the error.
	 * Otherwise, there's just a long line pointing at empty space.
	 * If there aren't any line breaks, the break positions contain npos,
	 * meaning we can safely use it as size in string::substr to indicate
	 * the whole length of the string */
	std::size_t
		breakBefore { functionStr.rfind( '\n', newErrPos ) },
		breakAfter  { functionStr. find( '\n', newErrPos ) };

	std::string dashes (
		newErrPos - ( breakBefore == npos ? 0 : breakBefore + indentSize ),
		'-'
	);

	std::string_view
		view { functionStr.data(), functionStr.size() },
		fstr1 { view.substr(0, breakAfter) },
		fstr2 { view.substr( std::min( breakAfter, view.size() ), npos ) };

	return fmt::format(
		"{}\n"
		"{}: {}{}\n"
		"{}{}: {}{}"
		"{}{}",
		bold( errorMsg ),
		msg1, pad, fstr1,
		msg2, bold(errPosStr), dashes, bold(red("^")),
		std::string( msg1.size() + pad.size(), ' ' ), fstr2
	);
}

FunctionParserWrapper::FunctionParserWrapper()
{
	/* make constants defined in "globals.h" available to user */
	m_parser.AddConstant("pi", math::pi);
	m_parser.AddConstant("e" , math::e );
	m_parser.AddConstant("g" , phys::g );
	
	m_parser.setEpsilon( epsilon );
	/* this was part of the very first design and no longer correct */
	// m_parser.setDelimiterChar( io::matSuffix );
}

FunctionParserWrapper::FunctionParserWrapper( std::string functionStr, bool optimise ) :
	FunctionParserWrapper() /* default constructor defines constants etc */
{
	parse( std::move(functionStr), optimise );
}


int FunctionParserWrapper::parse( std::string functionStr, bool optimise )
{
	/* tabs to spaces to get alignment in console output correct
	 * 4 spaces seems to be what tinyxml2 uses for XML node indentation */
	tabsToSpaces(functionStr, 4);
	/* only needed for composing the error message,
	 * but must be done before trimming. */
	std::size_t baseIndent { nLeftWhitespace(functionStr) };
	trim(functionStr);
	int errPos { m_parser.Parse( functionStr, std::string{m_vars} ) };
	
	/* errPos == -1 means no error, but checking the ParseErrorType makes more
	 * sense, because hitting the delimiter returns its position, which is not
	 * an error. */
	if ( m_parser.GetParseErrorType() != FunctionParser::FP_NO_ERROR ){
		throw FunctionParserError( composeFParserError(
			functionStr, errPos, m_parser.ErrorMsg(), baseIndent
		) );
	}
	
	m_functionString = std::move(functionStr);
	
	/* call eval() once with default values to catch possible errors */
	eval();

	/* Optimising the opcode is slow, and could lead to a performance 
	 * degradation, especially on clusters, where function string parsing has
	 * to be redone after each redistribution. */
	if (optimise) m_parser.Optimize();
	
	return errPos;
}


double FunctionParserWrapper::eval(
	double x, double y, double z, double t, double dt,
	double x_dom, double y_dom, double z_dom, double val_dom
) const {
	double vars[] = { x, y, z, t, dt, x_dom, y_dom, z_dom, val_dom };
	double result = m_parser.Eval( vars );

	auto throwWith = [&]( std::string&& message ){
		throw FunctionParserError(
			"Error in evaluation of function \"" + m_functionString + 
			"\" with x=" + std::to_string(x) +
			", y=" + std::to_string(y) +
			", z=" + std::to_string(z) +
			", t=" + std::to_string(t) +
			", dt=" + std::to_string(dt) +
			", x_dom=" + std::to_string(x_dom) +
			", y_dom=" + std::to_string(y_dom) +
			", z_dom=" + std::to_string(z_dom) +
			", val_dom=" + std::to_string(val_dom) +
			": " + message
		);
	};
	/* these are the error values according to the documentation */
	switch ( m_parser.EvalError() ){
		case 0: break;
		case 1:
			throwWith( "Division by zero" );
			break;
		case 2:
			throwWith( "Square root of negative value" );
			break;
		case 3:
			throwWith( "Logarithm of a negative value" );
			break;
		case 4:
			throwWith( "asin or acos of illegal value" );
			break;
		case 5:
			throwWith( "Maximum recursion level reached" );
			break;
		default:
			throwWith( "Unknown error" );
	}
	return result;
}

const std::string& FunctionParserWrapper::functionString() const {
	return m_functionString;
}

const FunctionParser& FunctionParserWrapper::parser() const { return m_parser; }


} // end namespace hms